<template>
    <div class="upload-component">
        <el-upload ref="uploadRef" :action="action" :headers="headers" :data="data" :name="name"
            :with-credentials="withCredentials" :multiple="multiple" :accept="accept" :on-preview="handlePreview"
            :on-remove="handleRemove" :on-success="handleSuccess" :on-error="handleError" :on-progress="handleProgress"
            :on-change="handleChange" :before-upload="beforeUpload" :before-remove="beforeRemove" :list-type="listType"
            :auto-upload="autoUpload" :file-list="fileList" :disabled="disabled" :limit="limit"
            :on-exceed="handleExceed" :drag="drag" :show-file-list="showFileList" class="upload-container">
            <!-- 拖拽上传 -->
            <template v-if="drag">
                <el-icon class="el-icon--upload"><upload-filled /></el-icon>
                <div class="el-upload__text">
                    将文件拖到此处，或<em>点击上传</em>
                </div>
                <template #tip>
                    <div class="el-upload__tip text-red">
                        {{ tip }}
                    </div>
                </template>
            </template>

            <!-- 图片上传 -->
            <template v-else-if="listType === 'picture-card'">
                <el-icon>
                    <Plus />
                </el-icon>
                <template #tip>
                    <div class="el-upload__tip">
                        {{ tip }}
                    </div>
                </template>
            </template>

            <!-- 普通上传 -->
            <template v-else>
                <el-button type="primary" :icon="Upload">选择文件</el-button>
                <template #tip>
                    <div class="el-upload__tip">
                        {{ tip }}
                    </div>
                </template>
            </template>
        </el-upload>

        <!-- 图片预览对话框 -->
        <el-dialog v-model="previewDialogVisible" title="图片预览" width="60%" center>
            <div class="preview-container">
                <el-image :src="previewImageUrl" fit="contain" style="width: 100%; max-height: 500px;" />
            </div>
        </el-dialog>

        <!-- 上传进度 -->
        <el-progress v-if="uploadProgress > 0 && uploadProgress < 100" :percentage="uploadProgress"
            :status="uploadStatus" class="upload-progress" />
    </div>
</template>

<script setup lang="ts">
/**
 * 文件上传组件
 * @author Wu.Liang
 * @description 支持图片和文件上传，支持预览、拖拽、进度显示等功能
 */
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload, UploadFilled } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile, UploadFile, UploadRawFile } from 'element-plus'
import { showError, showSuccess, showWarning } from '@/utils/message'

interface Props {
    // 上传地址
    action?: string
    // 请求头
    headers?: Record<string, any>
    // 上传额外数据
    data?: Record<string, any>
    // 上传文件字段名
    name?: string
    // 是否携带cookie
    withCredentials?: boolean
    // 是否支持多选
    multiple?: boolean
    // 接受的文件类型
    accept?: string
    // 列表类型
    listType?: 'text' | 'picture' | 'picture-card'
    // 是否自动上传
    autoUpload?: boolean
    // 文件列表
    modelValue?: UploadUserFile[]
    // 是否禁用
    disabled?: boolean
    // 最大上传数量
    limit?: number
    // 是否支持拖拽
    drag?: boolean
    // 是否显示文件列表
    showFileList?: boolean
    // 提示文字
    tip?: string
    // 文件大小限制（MB）
    maxSize?: number
    // 允许的文件类型
    allowedTypes?: string[]
}

const props = withDefaults(defineProps<Props>(), {
    action: '/api/common/upload',
    name: 'file',
    withCredentials: false,
    multiple: false,
    accept: '*',
    listType: 'text',
    autoUpload: true,
    disabled: false,
    limit: 10,
    drag: false,
    showFileList: true,
    tip: '请选择要上传的文件',
    maxSize: 10,
    allowedTypes: () => []
})

const emit = defineEmits<{
    'update:modelValue': [files: UploadUserFile[]]
    'on-success': [response: any, file: UploadFile, fileList: UploadUserFile[]]
    'on-error': [error: Error, file: UploadFile, fileList: UploadUserFile[]]
    'on-progress': [event: any, file: UploadFile, fileList: UploadUserFile[]]
    'on-change': [file: UploadFile, fileList: UploadUserFile[]]
    'on-preview': [file: UploadFile]
    'on-remove': [file: UploadFile, fileList: UploadUserFile[]]
}>()

const uploadRef = ref()
const previewDialogVisible = ref(false)
const previewImageUrl = ref('')
const uploadProgress = ref(0)
const uploadStatus = ref<'success' | 'exception' | undefined>()

// 文件列表
const fileList = computed({
    get: () => props.modelValue || [],
    set: (value) => emit('update:modelValue', value)
})

// 请求头（包含token）
const headers = computed(() => {
    const token = localStorage.getItem('token')
    return {
        ...props.headers,
        Authorization: token ? `Bearer ${token}` : ''
    }
})

// 上传前校验
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
    // 文件大小校验
    if (props.maxSize && rawFile.size / 1024 / 1024 > props.maxSize) {
        showError(`文件大小不能超过 ${props.maxSize}MB`)
        return false
    }

    // 文件类型校验
    if (props.allowedTypes.length > 0) {
        const fileType = rawFile.type
        const fileName = rawFile.name
        const fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()

        const isTypeAllowed = props.allowedTypes.some(type => {
            return fileType.includes(type) || fileExt === type
        })

        if (!isTypeAllowed) {
            showError(`只允许上传 ${props.allowedTypes.join(', ')} 格式的文件`)
            return false
        }
    }

    uploadProgress.value = 0
    uploadStatus.value = undefined
    return true
}

// 删除前确认
const beforeRemove: UploadProps['beforeRemove'] = (file) => {
    return ElMessageBox.confirm('确定要删除这个文件吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => true).catch(() => false)
}

// 文件预览
const handlePreview: UploadProps['onPreview'] = (file) => {
    if (file.url) {
        // 判断是否为图片
        const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
        const fileExt = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()

        if (imageTypes.includes(fileExt)) {
            previewImageUrl.value = file.url
            previewDialogVisible.value = true
        } else {
            // 非图片文件，下载
            const link = document.createElement('a')
            link.href = file.url
            link.download = file.name
            link.click()
        }
    }
    emit('on-preview', file)
}

// 文件移除
const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
    emit('update:modelValue', uploadFiles)
    emit('on-remove', file, uploadFiles)
}

// 上传成功
const handleSuccess: UploadProps['onSuccess'] = (response, file, uploadFiles) => {
    uploadProgress.value = 100
    uploadStatus.value = 'success'

    // 更新文件URL
    if (response && response.data && response.data.url) {
        file.url = response.data.url
    }

    emit('update:modelValue', uploadFiles)
    emit('on-success', response, file, uploadFiles)
    showSuccess('上传成功')

    // 清除进度条
    setTimeout(() => {
        uploadProgress.value = 0
        uploadStatus.value = undefined
    }, 2000)
}

// 上传失败
const handleError: UploadProps['onError'] = (error, file, uploadFiles) => {
    uploadProgress.value = 100
    uploadStatus.value = 'exception'
    emit('on-error', error, file, uploadFiles)
    showError('上传失败')

    setTimeout(() => {
        uploadProgress.value = 0
        uploadStatus.value = undefined
    }, 2000)
}

// 上传进度
const handleProgress: UploadProps['onProgress'] = (event, file, uploadFiles) => {
    uploadProgress.value = Math.round(event.percent || 0)
    emit('on-progress', event, file, uploadFiles)
}

// 文件改变
const handleChange: UploadProps['onChange'] = (file, uploadFiles) => {
    emit('update:modelValue', uploadFiles)
    emit('on-change', file, uploadFiles)
}

// 文件超出限制
const handleExceed: UploadProps['onExceed'] = (files) => {
    showWarning(`最多只能上传 ${props.limit} 个文件`)
}

// 清空文件列表
const clearFiles = () => {
    uploadRef.value?.clearFiles()
}

// 手动上传
const submit = () => {
    uploadRef.value?.submit()
}

// 暂停上传
const abort = () => {
    uploadRef.value?.abort()
}

defineExpose({
    clearFiles,
    submit,
    abort
})
</script>

<style lang="scss" scoped>
.upload-component {
    .upload-container {
        width: 100%;
    }

    .upload-progress {
        margin-top: 10px;
    }

    .preview-container {
        text-align: center;
    }

    :deep(.el-upload--picture-card) {
        width: 100px;
        height: 100px;
        line-height: 100px;
    }

    :deep(.el-upload-list--picture-card .el-upload-list__item) {
        width: 100px;
        height: 100px;
    }

    :deep(.el-upload-dragger) {
        width: 100%;
        height: 120px;
    }
}
</style>
